package org.liquidByte.simpleChat.communication;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ChannelController implements ChannelFaultedEventListener {
	private Lock lock;
	private ExecutorService threadPool;
	private RemoteCallHandler callHandler;
	private List<ListenerTask> listeners;
	private List<Channel> channels;
	private List<ChannelListeningErrorEventListener> listenErrorListeners;
	private List<ChannelFaultedEventListener> channelFaultedListeners;
	
	public ChannelController(RemoteCallHandler callHandler) {
		if (callHandler == null) {
			throw new NullPointerException("callHandler is null");
		}
		this.callHandler = callHandler;
		this.channels = new ArrayList<Channel>();
		this.listeners = new ArrayList<ListenerTask>();
		this.threadPool = Executors.newCachedThreadPool();
		this.lock = new ReentrantLock();
		this.listenErrorListeners = new ArrayList<ChannelListeningErrorEventListener>();
		this.channelFaultedListeners = new ArrayList<ChannelFaultedEventListener>();
	}
	
	public void addListener(ChannelListeningErrorEventListener listener) {
		this.lock.lock();
		try {
			this.listenErrorListeners.add(listener);
		}
		finally {
			this.lock.unlock();
		}	
	}
	public void removeListener(ChannelListeningErrorEventListener listener) {
		this.lock.lock();
		try {
			this.listenErrorListeners.remove(listener);
		}
		finally {
			this.lock.unlock();
		}
	}
	private void unbind(Channel channel) {
		this.lock.lock();
		try {
			this.channels.remove(channel);
		}
		finally {
			this.lock.unlock();
		}
	}
	private void bind(ServerSocket serverSocket) throws SocketException {
		this.lock.lock();
		try {
			ListenerTask task = new ListenerTask(serverSocket, this);
			this.threadPool.execute(task);
			this.listeners.add(task);
		}
		finally {
			this.lock.unlock();
		}
	}
	private Channel bind(Socket socket) throws SocketException {
		this.lock.lock();
		try {
			Channel channel = new Channel(socket);
			channel.init(this.callHandler);
			channel.addListener((ChannelFaultedEventListener)this);
			this.channels.add(channel);
			return channel;
		}
		finally {
			this.lock.unlock();
		}
	}
	private synchronized void onListenFaulted(ListenerTask task, Throwable e) {
		this.free(task);
		for (ChannelListeningErrorEventListener listener : this.listenErrorListeners) {
			listener.onListeningError(task.serverSocket.getLocalPort(), e);
		}
	}
	private void free(ListenerTask task) {
		this.lock.lock();
		try {
			this.listeners.remove(task);
		}
		finally {
			this.lock.unlock();
		}
	}
	public void listen(int port) throws IOException {
		this.bind(new ServerSocket(port));
	}
	public Channel connect(String host, int port) throws UnknownHostException, IOException {
		return this.bind(new Socket(host, port));
	}
	public void dispose() {
		List<ListenerTask> listeners  = this.listeners;
		this.listeners = null;
		for (ListenerTask task : listeners) {
			try {
				task.shutdown();
			} catch (InterruptedException e) { }
		}		
		for (Channel channel : this.channels) {
			channel.dispose();
		}
	}
	
	private class ListenerTask implements Runnable {
		private ServerSocket serverSocket;
		private ChannelController controller;
		private Lock lock;
		private Condition exited;
		private boolean executing;
		
		public ListenerTask(ServerSocket serverSocket, ChannelController controller) throws SocketException {
			if (serverSocket == null) {
				throw new NullPointerException("serverSocket is null");
			}
			if (controller == null) {
				throw new NullPointerException("controller is null");
			}
			this.serverSocket = serverSocket;
			this.controller = controller;
			this.lock = new ReentrantLock();
			this.exited = this.lock.newCondition();
			this.executing = false;
			serverSocket.setSoTimeout(300);
		}

		@Override
		public void run() {
			this.lock.lock();
			try {
				this.executing = true;
			}
			finally {
				this.lock.unlock();
			}
			
			while (this.executing) {
				try {
					Socket socket = this.serverSocket.accept();
					this.controller.bind(socket);
				}
				catch(SocketTimeoutException e){
					
				}
				catch (IOException e) {
					// error listener erro
					this.controller.onListenFaulted(this, e);
					break;
				}
			}
			
			this.lock.lock();
			try {
				this.exited.signal();
				this.executing = false;
			}	
			finally {
				this.lock.unlock();
			}
		}
		
		public void shutdown() throws InterruptedException {
			this.lock.lock();
			try {
				if (this.executing) {
					this.executing = false;
					this.exited.await();
				}
			}
			catch (InterruptedException e) {
				this.executing = true;
				throw e;
			}
			finally {
				this.lock.unlock();
			}
		}
	}
	
	public void addListener(ChannelFaultedEventListener listener) {
		this.lock.lock();
		try {
			this.channelFaultedListeners.add(listener);
		}
		finally {
			this.lock.unlock();
		}
	}
	public void removeListener(ChannelFaultedEventListener listener) {
		this.lock.lock();
		try {
			this.channelFaultedListeners.remove(listener);
		}
		finally {
			this.lock.unlock();
		}
	}

	@Override
	public synchronized void faulted(Channel channel, Throwable cause) {
		this.lock.lock();
		try {
			this.unbind(channel);
			for (ChannelFaultedEventListener listener : this.channelFaultedListeners) {
				listener.faulted(channel, cause);
			}
		}
		finally {
			this.lock.unlock();
		}
	}
}
